﻿namespace EnterpriseLibrary.Caching.AzureBackingStore
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;

    using EnterpriseLibrary.Caching.AzureBackingStore.Configuration;

    using Microsoft.ApplicationServer.Caching;
    using Microsoft.Practices.EnterpriseLibrary.Caching;
    using Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations;
    using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;

    [ConfigurationElementType(typeof(AzureCacheStorageData))]
    public class AzureBackingStore : BaseBackingStore
    {
        #region Fields

        readonly string dataCacheName;
        readonly string dataCacheRegionName;
        readonly IStorageEncryptionProvider encryptionProvider;
        readonly bool isRegionSupported;

        static AzureDataCacheHelper azureDataCacheHelper;

        #endregion Fields

        #region Constructors

        public AzureBackingStore(string dataCache, string dataCacheRegion, IStorageEncryptionProvider provider)
        {
            this.encryptionProvider = provider;
            this.dataCacheName = dataCache;
            this.dataCacheRegionName = dataCacheRegion;

            this.isRegionSupported = !string.IsNullOrWhiteSpace(this.dataCacheRegionName);

            if (azureDataCacheHelper == null)
            {
                azureDataCacheHelper = new AzureDataCacheHelper();
                azureDataCacheHelper.Initilize();
            }
        }

        #endregion Constructors

        #region Properties

        public override int Count
        {
            get
            {
                DataCache dataCache = this.isRegionSupported ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName)
                    : azureDataCacheHelper.GetDataCache(this.dataCacheName);

                int returnValue = this.isRegionSupported ? dataCache.GetObjectsInRegion(this.dataCacheRegionName).Count()
                    : dataCache.GetSystemRegions().SelectMany(dataCache.GetObjectsInRegion).Count();

                return returnValue;
            }
        }

        #endregion Properties

        #region Methods

        public override void Flush()
        {
            DataCache dataCache = this.isRegionSupported ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName) : azureDataCacheHelper.GetDataCache(this.dataCacheName);
            if (this.isRegionSupported)
            {
                dataCache.ClearRegion(this.dataCacheRegionName);
            }
            else
            {
                foreach (string regionName in dataCache.GetSystemRegions())
                {
                    dataCache.ClearRegion(regionName);
                }
            }
        }

        protected override void AddNewItem(int storageKey, CacheItem newItem)
        {
            AzureCacheItem cacheItem = this.ConvertCacheItemToAzureCacheItem(newItem);
            DataCache dataCache = this.isRegionSupported
                                      ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName)
                                      : azureDataCacheHelper.GetDataCache(this.dataCacheName);

            if (this.isRegionSupported)
            {
                dataCache.Add(this.GetAzureStorageKey(storageKey), cacheItem, this.dataCacheRegionName);
            }
            else
            {
                dataCache.Add(this.GetAzureStorageKey(storageKey), cacheItem);
            }
        }

        protected override Hashtable LoadDataFromStore()
        {
            Hashtable itemsLoadedFromStore = new Hashtable();
            DataCache dataCache = this.isRegionSupported ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName) : azureDataCacheHelper.GetDataCache(this.dataCacheName);

            IEnumerable<KeyValuePair<string, object>> list = this.isRegionSupported ?
                dataCache.GetObjectsInRegion(this.dataCacheRegionName) :
                dataCache.GetSystemRegions().SelectMany(dataCache.GetObjectsInRegion);

            foreach (var keyValuePair in list)
            {
                itemsLoadedFromStore.Add(
                    keyValuePair.Key, this.ConvertAzureCacheItemToCacheItem((AzureCacheItem)keyValuePair.Value));
            }

            return itemsLoadedFromStore;
        }

        protected override void Remove(int storageKey)
        {
            DataCache dataCache = this.isRegionSupported ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName) : azureDataCacheHelper.GetDataCache(this.dataCacheName);

            if (this.isRegionSupported)
            {
                dataCache.Remove(storageKey.ToString(NumberFormatInfo.InvariantInfo), this.dataCacheRegionName);
            }
            else
            {
                dataCache.Remove(storageKey.ToString(NumberFormatInfo.InvariantInfo));
            }
        }

        protected override void RemoveOldItem(int storageKey)
        {
            this.Remove(storageKey);
        }

        protected override void UpdateLastAccessedTime(int storageKey, DateTime timestamp)
        {
            DataCache dataCache = this.isRegionSupported ? azureDataCacheHelper.GetDataCache(this.dataCacheName, this.dataCacheRegionName) : azureDataCacheHelper.GetDataCache(this.dataCacheName);
            if (this.isRegionSupported)
            {
                AzureCacheItem item = (AzureCacheItem)dataCache.Get(this.GetAzureStorageKey(storageKey), this.dataCacheRegionName);
                item.LastAccessedTime = timestamp;
                dataCache.Put(this.GetAzureStorageKey(storageKey), item, this.dataCacheRegionName);
            }
            else
            {
                AzureCacheItem item = (AzureCacheItem)dataCache.Get(this.GetAzureStorageKey(storageKey));
                item.LastAccessedTime = timestamp;
                dataCache.Put(this.GetAzureStorageKey(storageKey), item);
            }
        }

        private CacheItem ConvertAzureCacheItemToCacheItem(AzureCacheItem cacheItem)
        {
            object cachedData = cacheItem.Data;

            if (this.encryptionProvider != null)
            {
                byte[] deSerializedData = this.DecryptValue((byte[])cacheItem.Data);
                cachedData = SerializationUtility.ToObject(deSerializedData);
            }

            return new CacheItem(
                cacheItem.LastAccessedTime,
                cacheItem.Key,
                cachedData,
                cacheItem.ScavengingPriority,
                cacheItem.RefreshAction,
                cacheItem.Expirations);
        }

        private AzureCacheItem ConvertCacheItemToAzureCacheItem(CacheItem cacheItem)
        {
            object cachedData = cacheItem.Value;

            if (this.encryptionProvider != null)
            {
                byte[] serializedData = SerializationUtility.ToBytes(cacheItem.Value);
                cachedData = this.EncryptValue(serializedData);
            }

            return new AzureCacheItem(
                cacheItem.Key,
                cachedData,
                cacheItem.RefreshAction,
                cacheItem.GetExpirations(),
                cacheItem.ScavengingPriority,
                cacheItem.LastAccessedTime);
        }

        private byte[] DecryptValue(byte[] fieldBytes)
        {
            if (this.encryptionProvider != null)
            {
                fieldBytes = this.encryptionProvider.Decrypt(fieldBytes);
            }

            return fieldBytes;
        }

        private byte[] EncryptValue(byte[] valueBytes)
        {
            if (this.encryptionProvider != null)
            {
                valueBytes = this.encryptionProvider.Encrypt(valueBytes);
            }

            return valueBytes;
        }

        private string GetAzureStorageKey(int storageKey)
        {
            return storageKey.ToString(NumberFormatInfo.InvariantInfo);
        }

        #endregion Methods
    }
}